/**
 * Copyright (c) 2008 IBM Corporation and others.
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-v2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 * IBM Corporation - initial API and implementation
 */
package org.eclipse.egf.core.domain;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.commands.operations.UndoContext;
import org.eclipse.egf.core.l10n.EGFCoreMessages;
import org.eclipse.emf.common.command.CommandStack;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.transaction.NotificationFilter;
import org.eclipse.emf.transaction.ResourceSetChangeEvent;
import org.eclipse.emf.transaction.ResourceSetListener;
import org.eclipse.emf.transaction.ResourceSetListenerImpl;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.emf.transaction.TransactionalEditingDomain.Lifecycle;
import org.eclipse.emf.transaction.TransactionalEditingDomainEvent;
import org.eclipse.emf.transaction.TransactionalEditingDomainListenerImpl;
import org.eclipse.emf.transaction.util.TransactionUtil;
import org.eclipse.emf.workspace.IWorkspaceCommandStack;
import org.eclipse.emf.workspace.ResourceUndoContext;
import org.eclipse.osgi.util.NLS;

/**
 * Manages the <code>isModified</code> state of resources in a given editing
 * domain as operations are executed, undone and redone on the operation
 * history.
 * <P>
 * This allows clients to use the <code>isModified</code> state of a resource to
 * determine whether or not the resource is dirty and can be saved.
 * 
 * @author ldamus
 * @since 1.2
 *        comes from org.eclipse.gmf.runtime.emf.core.resources.GMFResourceModificationManager
 */
public class ResourceModificationManager {

    /**
     * Keeps track of the modification manager for each editing domain. Only one
     * modification manager can ever be created for a given editing domain. Keys
     * are WeakReferences because the modification manager has a reference back
     * to its editing domain key.
     */
    private static Map<TransactionalEditingDomain, WeakReference<ResourceModificationManager>> managerRegistry = new WeakHashMap<TransactionalEditingDomain, WeakReference<ResourceModificationManager>>();

    /**
     * Creates a new resource modification manager for <code>domain</code>, if
     * the <code>domain</code>'s command stack is integrated with an
     * <code>IOperationHistory</code>. The <code>isModified</code> state of a
     * resource in <code>domain</code> will be set to <code>false</code> when
     * the last operation affecting that resource is undone on the history.
     * 
     * @param domain
     *          the editing domain
     * @return the resource modification manager, or <code>null</code> if
     *         <code>domain</code> is not integrated with an operation history
     */
    public static synchronized ResourceModificationManager manage(TransactionalEditingDomain domain) {

        // make sure we only instantiate one manager per editing domain
        WeakReference<ResourceModificationManager> reference = managerRegistry.get(domain);
        ResourceModificationManager result = reference != null ? reference.get() : null;

        if (result == null) {
            CommandStack stack = domain.getCommandStack();

            if (stack instanceof IWorkspaceCommandStack) {
                IOperationHistory history = ((IWorkspaceCommandStack) stack).getOperationHistory();

                if (history != null) {
                    final ResourceModificationManager manager = new ResourceModificationManager(domain, history);
                    managerRegistry.put(domain, new WeakReference<ResourceModificationManager>(manager));
                    result = manager;

                    // dispose the modification manager when the domain is
                    // disposed
                    Lifecycle lifecycle = TransactionUtil.getAdapter(domain, Lifecycle.class);

                    if (lifecycle != null) {
                        lifecycle.addTransactionalEditingDomainListener(new TransactionalEditingDomainListenerImpl() {

                            @Override
                            public void editingDomainDisposing(TransactionalEditingDomainEvent event) {
                                manager.dispose();
                            }
                        });
                    }
                }
            }
        }
        return result;
    }

    /**
     * A filter matching "resource is no longer modified" events.
     */
    private static final NotificationFilter RESOURCE_UNMODIFIED = new NotificationFilter.Custom() {

        @Override
        public boolean matches(Notification notification) {
            return (notification.getNotifier() instanceof Resource) && (notification.getFeatureID(Resource.class) == Resource.RESOURCE__IS_MODIFIED) && notification.getOldBooleanValue() && !notification.getNewBooleanValue();
        }
    };

    private TransactionalEditingDomain domain;

    private IOperationHistory history;

    private ResourceSetListener domainListener;

    private IOperationHistoryListener historyListener;

    private Map<Resource, IUndoContext> saveContexts;

    private IUndoableOperation currentOperation;

    /**
     * Private constructor to prevent instantiation by clients. Clients must use {@link #manage(TransactionalEditingDomain)} to construct a new instance.
     * 
     * @param domain
     *          the editing domain
     * @param history
     *          the operation history
     */
    private ResourceModificationManager(TransactionalEditingDomain domain, IOperationHistory history) {

        this.domain = domain;
        this.history = history;

        domain.addResourceSetListener(getDomainListener());
        history.addOperationHistoryListener(getHistoryListener());
    }

    /**
     * Gets the resource set listener listener, which manages the save-point
     * context for operations executed on the history when the resource is saved
     * or unloaded.
     * 
     * @return the resource set listener
     */
    private ResourceSetListener getDomainListener() {

        if (domainListener == null) {
            domainListener = new ResourceSetListenerImpl(RESOURCE_UNMODIFIED.or(NotificationFilter.RESOURCE_UNLOADED)) {

                @Override
                public void resourceSetChanged(ResourceSetChangeEvent event) {

                    for (Notification n : event.getNotifications()) {
                        Resource resource = (Resource) n.getNotifier();

                        switch (n.getFeatureID(Resource.class)) {

                            case Resource.RESOURCE__IS_MODIFIED:
                                applySaveContext(resource);
                                break;

                            case Resource.RESOURCE__IS_LOADED:
                                disposeSaveContext(resource);
                                break;
                        }
                    }
                }

                @Override
                public boolean isPostcommitOnly() {
                    return true;
                }
            };
        }
        return domainListener;
    }

    /**
     * Gets the operation history listener, which manages the
     * <code>isModified</code> state of the resources.
     * 
     * @return the operation history listener
     */
    private IOperationHistoryListener getHistoryListener() {

        if (historyListener == null) {
            historyListener = new IOperationHistoryListener() {

                public void historyNotification(OperationHistoryEvent event) {
                    int type = event.getEventType();

                    switch (type) {

                        case OperationHistoryEvent.ABOUT_TO_EXECUTE:
                        case OperationHistoryEvent.ABOUT_TO_UNDO:
                        case OperationHistoryEvent.ABOUT_TO_REDO:
                            // Remember the operation in order to apply the
                            // save context to it if the isModified is set to false
                            // during execute, undo or redo. For undo, the save
                            // context goes on next undoable operation on the
                            // history.
                            currentOperation = event.getOperation();
                            break;

                        case OperationHistoryEvent.OPERATION_NOT_OK:
                            currentOperation = null;
                            break;

                        case OperationHistoryEvent.DONE: {
                            currentOperation = null;

                            IUndoableOperation operation = event.getOperation();
                            Set<Resource> affectedResources = getAffectedResourcesInDomain(operation);

                            for (Resource r : affectedResources) {
                                ResourceUndoContext context = new ResourceUndoContext(domain, r);
                                IUndoableOperation[] undoHistory = history.getUndoHistory(context);

                                if (undoHistory.length >= history.getLimit(context)) {
                                    // We've reached the limit for this context;
                                    // initialize the save context to indicate that
                                    // we can't undo to the last saved state
                                    getSaveContext(r);
                                }
                            }
                            break;
                        }

                        case OperationHistoryEvent.UNDONE:
                        case OperationHistoryEvent.REDONE: {
                            currentOperation = null;

                            IUndoableOperation operation = event.getOperation();
                            Set<Resource> affectedResources = getAffectedResourcesInDomain(operation);

                            for (Resource r : affectedResources) {
                                IUndoContext saveContext = getSaveContexts().get(r);
                                IUndoableOperation nextUndoableOperation = getNextUndoableOperation(r);

                                boolean atStart = saveContext == null && nextUndoableOperation == null;

                                boolean atSaveContext = saveContext != null && nextUndoableOperation != null && nextUndoableOperation.hasContext(saveContext);

                                if (atStart || atSaveContext) {
                                    r.setModified(false);
                                }
                            }
                        }
                    }
                }
            };
        }
        return historyListener;
    }

    private Map<Resource, IUndoContext> getSaveContexts() {
        if (saveContexts == null) {
            saveContexts = new HashMap<Resource, IUndoContext>();
        }
        return saveContexts;
    }

    private IUndoableOperation getNextUndoableOperation(Resource resource) {
        return history.getUndoOperation(new ResourceUndoContext(domain, resource));
    }

    private IUndoContext getSaveContext(final Resource resource) {
        IUndoContext saveContext = getSaveContexts().get(resource);

        if (saveContext == null) {
            saveContext = new UndoContext() {

                @Override
                public String getLabel() {
                    return NLS.bind(EGFCoreMessages.saveContextLabel, resource.getURI());
                }

                @Override
                public String toString() {
                    return getLabel();
                }
            };

            getSaveContexts().put(resource, saveContext);
        }
        return saveContext;
    }

    private Set<Resource> getAffectedResourcesInDomain(IUndoableOperation operation) {

        Set<Resource> result = new HashSet<Resource>();
        Set<Resource> affectedResources = ResourceUndoContext.getAffectedResources(operation);

        for (Resource resource : affectedResources) {
            ResourceSet resourceSet = resource.getResourceSet();

            if (domain.getResourceSet().equals(resourceSet)) {
                result.add(resource);
            }
        }
        return result;
    }

    private void applySaveContext(Resource resource) {
        IUndoContext saveContext = getSaveContexts().get(resource);

        if (saveContext != null) {
            // Remove the save context from existing operations
            IUndoableOperation[] undoableOperations = history.getUndoHistory(saveContext);
            for (IUndoableOperation op : undoableOperations) {
                op.removeContext(saveContext);
            }

            IUndoableOperation[] redoableOperations = history.getRedoHistory(saveContext);
            for (IUndoableOperation op : redoableOperations) {
                op.removeContext(saveContext);
            }
        }

        IUndoableOperation operation = null;
        IUndoableOperation nextUndoable = getNextUndoableOperation(resource);

        if (currentOperation != null) {

            if (currentOperation == nextUndoable) {
                // we're undoing; get the previous operation on the history
                IUndoableOperation[] undoableOperations = history.getUndoHistory(new ResourceUndoContext(domain, resource));

                for (int i = undoableOperations.length - 1; i >= 0; i--) {
                    if (currentOperation != undoableOperations[i]) {
                        operation = undoableOperations[i];
                        break;
                    }
                }
            } else {
                operation = currentOperation;
            }
        } else {
            operation = nextUndoable;
        }

        if (operation != null) {
            // apply the save context
            operation.addContext(getSaveContext(resource));

        } else {
            // clear the save context; required if we save after undoing the
            // last thing on the stack
            getSaveContexts().remove(resource);
        }
    }

    private void disposeSaveContext(Resource resource) {
        IUndoContext saveContext = getSaveContexts().get(resource);

        if (saveContext != null) {
            history.dispose(saveContext, true, true, true);
            getSaveContexts().remove(resource);
        }
    }

    private void dispose() {

        managerRegistry.remove(domain);

        if (saveContexts != null) {
            for (Resource r : saveContexts.keySet()) {
                disposeSaveContext(r);
            }
        }
        if (domainListener != null) {
            domain.removeResourceSetListener(domainListener);
        }
        if (historyListener != null) {
            history.removeOperationHistoryListener(historyListener);
        }

        currentOperation = null;
        domain = null;
        domainListener = null;
        history = null;
        historyListener = null;
        saveContexts = null;
    }

}
